package org.lgh.catalina.startup;

import org.lgh.catalina.security.SecurityClassLoad;
import org.lgh.juli.logging.Log;
import org.lgh.juli.logging.LogFactory;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.lgh.catalina.startup.ClassLoaderFactory.Repository;
import org.lgh.catalina.startup.ClassLoaderFactory.RepositoryType;

public final class Bootstrap {

    /**
     * liang1 start-preinit 简单工厂设计模式
     * 简单工厂设计模式
     */
    private static final Log log = LogFactory.getLog(Bootstrap.class);

    /**
     * Daemon object used by main.
     */
    private static final Object daemonLock = new Object();

    /**
     * liang8 start-preinit volatile
     *
     * 1 保证内存可见性
     * 此时如果对变量i加上volatile关键字修饰的话，它可以保证当A线程对变量i值做了变动之后，会立即刷回到主内存中，
     * 而其它线程读取到该变量的值也作废，强迫重新从主内存中读取该变量的值，这样在任何时刻，AB线程总是会看到变量i的同一个值。
     *
     * 2 禁止指令重排序
     * 指令的执行顺序并不一定会像我们编写的顺序那样执行，为了保证执行上的效率，JVM（包括CPU）可能会对指令进行重排序。比方说下面的代码：
     * int i = 1;
     * int j = 2;
     *  上述的两条赋值语句在同一个线程之中，根据程序上的次序，“int i = 1;”的操作要先行发生于“int j = 2;”，
     *  但是“int j = 2;”的代码完全可能会被处理器先执行。JVM会保证在单线程的情况下，重排序后的执行结果会和重排序之前的结果一致。
     *  但是在多线程的场景下就不一定了。最典型的例子就是双重检查加锁版的单例实现，代码如下所示：
     *  public class Singleton {
     *     private volatile static Singleton instance;
     *     private Singleton() {}
     *     public static Singleton getInstance() {
     *         if (instance == null) {
     *             synchronized (Singleton.class) {
     *                 if (instance == null) {
     *                     instance = new Singleton();
     *                 }
     *             }
     *         }
     *         return instance;
     *     }
     * }
     * 由上可以看到，instance变量被volatile关键字所修饰，但是如果去掉该关键字，就不能保证该代码执行的正确性。
     * 这是因为“instance = new Singleton();”这行代码并不是原子操作，其在JVM中被分为如下三个阶段执行：
     *      为instance分配内存
     *      初始化instance
     *      将instance变量指向分配的内存空间
     * 由于JVM可能存在重排序，上述的二三步骤没有依赖的关系，可能会出现先执行第三步，后执行第二步的情况。
     * 也就是说可能会出现instance变量还没初始化完成，其他线程就已经判断了该变量值不为null，
     * 结果返回了一个没有初始化完成的半成品的情况。而加上volatile关键字修饰后，可以保证instance变量的操作不会被JVM所重排序，
     * 每个线程都是按照上述一二三的步骤顺序的执行，这样就不会出现问题。
     */
    private static volatile Bootstrap daemon = null;

    private static final File catalinaBaseFile;
    private static final File catalinaHomeFile;

    /**
     *  liang9 start-preint 正则表达式的基本使用
     *  \表示转义，表示原字符
     *  []用于确定中括号表达式的开始和结束位置。要匹配这些字符，请使用\[和\]
     *  星号指定前面子表达式可以出现零次或多次。要匹配 * 字符本身，请使用\*
     *  | 指定两项之间任选一项。如果要匹配丨字符本身，请使用\|
     *  本表达式，针对 "abcd,efgh,zcs",获取其中的字符串
     */
    private static final Pattern PATH_PATTERN = Pattern.compile("(\"[^\"]*\")|(([^,])*)");

    /**
     * 总结来说就是设置了两个值，默认开发情况就是当前的工程名
     * 如果是部署的项目，则可能是jar也可能是部署的应用文件夹
     * 这里有系统参数设置
     */
    static {

        /**
         * liang10 start-preinit 获取项目的根路径，永远不为空
         * 以下可以看到所有的系统默认属性，主要是系统环境信息
         * 操作系统、jdk版本，分隔符，项目根路径，path路径，语言及编码，虚拟机
         */
        // Will always be non-null
        // 返回当前工程的根路径
        String userDir = System.getProperty("user.dir");
//        Properties properties = System.getProperties();
//        for (Object o : properties.keySet()) {
//            System.out.println(o.toString()+" : " +System.getProperty(o.toString()));
//        }

        // 优先从系统配置中获取，优先级第二，这是可扩展的，springboot可以配置
        // 未配置，就是null
        String home = System.getProperty(Constants.CATALINA_HOME_PROP);
        File homeFile = null;

        if (home != null) {
            File f = new File(home);
            try {
                // 规范文件是指文件的绝对且唯一的路径，它会去掉路径中的冗余部分，比如“.”或“..”
                homeFile = f.getCanonicalFile(); //规范路径名
            } catch (IOException ioe) {
                homeFile = f.getAbsoluteFile();
            }
        }

        if (homeFile == null) {
            /**
             * liang11 start-preint 优先启动jar包，优先级第二
             */
            // First fall-back. See if current directory is a bin directory
            // in a normal Tomcat install
            File bootstrapJar = new File(userDir, "bootstrap.jar");

            if (bootstrapJar.exists()) {
                File f = new File(userDir, "..");
                try {
                    homeFile = f.getCanonicalFile();
                } catch (IOException ioe) {
                    homeFile = f.getAbsoluteFile();
                }
            }
        }

        if (homeFile == null) {
            // Second fall-back. Use current directory
            File f = new File(userDir);
            try {
                homeFile = f.getCanonicalFile();
            } catch (IOException ioe) {
                homeFile = f.getAbsoluteFile();
            }
        }

        catalinaHomeFile = homeFile;
        //安装目录设置到系统属性中，方便后面获取
        System.setProperty(Constants.CATALINA_HOME_PROP, catalinaHomeFile.getPath());

        // Then base
        String base = System.getProperty(Constants.CATALINA_BASE_PROP);
        if (base == null) {
            catalinaBaseFile = catalinaHomeFile;
        } else {
            File baseFile = new File(base);
            try {
                baseFile = baseFile.getCanonicalFile();
            } catch (IOException ioe) {
                baseFile = baseFile.getAbsoluteFile();
            }
            catalinaBaseFile = baseFile;
        }
        System.setProperty(Constants.CATALINA_BASE_PROP, catalinaBaseFile.getPath());
    }

    // -------------------------------------------------------------- Variables
    /**
     * Daemon reference.
     */
    private Object catalinaDaemon = null;

    ClassLoader commonLoader = null;
    ClassLoader catalinaLoader = null;
    ClassLoader sharedLoader = null;

    // -------------------------------------------------------- Private Methods

    /**
     * main方法第一步：初始化类加载器
     * 默认本地开发，就加载了是三个相同的类加载器 commonLoader catalinaLoader  sharedLoader
     * 可以通过catalina配置，可以初始化出不同的类加载器，相应的加载不同的url
     * CommonLoader：加载Tomcat所需要的jar包和class文件，可以被Tomcat容器本身以及各个Webapp访问；
     * CatalinaLoader：Tomcat容器私有的类加载器，加载路径中的class对于Webapp不可见；
     * SharedLoader：各个Webapp共享的类加载器，加载路径中的class对于所有Webapp可见，对于Tomcat不可见；
     *
     */
    private void initClassLoaders() {
        try {
            commonLoader = createClassLoader("common", null);
            if (commonLoader == null) {
                // no config file, default to this loader - we might be in a 'single' env.
                commonLoader = this.getClass().getClassLoader();
            }
            catalinaLoader = createClassLoader("server", commonLoader);
            sharedLoader = createClassLoader("shared", commonLoader);
        } catch (Throwable t) {
            handleThrowable(t);
            log.error("Class loader creation threw exception", t);
            System.exit(1);
        }
    }


    /**
     * 创建类加载器通用方法，默认tomcat加载common share server/catalina三种
     * @param name
     * @param parent
     * @return
     * @throws Exception
     */
    private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception {
        //common.loader -> "${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
        String value = CatalinaProperties.getProperty(name + ".loader");
        if ((value == null) || (value.equals(""))) {
            return parent;
        }

        value = replace(value);

        List<Repository> repositories = new ArrayList<>();

        String[] repositoryPaths = getPaths(value);

        for (String repository : repositoryPaths) {
            // Check for a JAR URL repository
            try {
                URI uri = new URI(repository);
                @SuppressWarnings("unused")
                URL url = uri.toURL();
                repositories.add(new Repository(repository, RepositoryType.URL));
                continue;
            } catch (IllegalArgumentException | MalformedURLException | URISyntaxException e) {
                // Ignore
            }

            // Local repository
            if (repository.endsWith("*.jar")) {
                repository = repository.substring(0, repository.length() - "*.jar".length());
                repositories.add(new Repository(repository, RepositoryType.GLOB));
            } else if (repository.endsWith(".jar")) {
                repositories.add(new Repository(repository, RepositoryType.JAR));
            } else {
                repositories.add(new Repository(repository, RepositoryType.DIR));
            }
        }

        return ClassLoaderFactory.createClassLoader(repositories, parent);
    }

    /**
     * System property replacement in the given string.
     *
     * 替换${},可学习一二
     *
     * @param str The original string
     *
     * @return the modified string
     */
    protected String replace(String str) {
        // Implementation is copied from ClassLoaderLogManager.replace(),
        // but added special processing for catalina.home and catalina.base.
        String result = str;
        int pos_start = str.indexOf("${");
        if (pos_start >= 0) {
            StringBuilder builder = new StringBuilder();
            int pos_end = -1;
            while (pos_start >= 0) {
                builder.append(str, pos_end + 1, pos_start);
                pos_end = str.indexOf('}', pos_start + 2);
                if (pos_end < 0) {
                    pos_end = pos_start - 1;
                    break;
                }
                String propName = str.substring(pos_start + 2, pos_end);
                String replacement;
                if (propName.length() == 0) {
                    replacement = null;
                } else if (Constants.CATALINA_HOME_PROP.equals(propName)) {
                    replacement = getCatalinaHome();
                } else if (Constants.CATALINA_BASE_PROP.equals(propName)) {
                    replacement = getCatalinaBase();
                } else {
                    replacement = System.getProperty(propName);
                }
                if (replacement != null) {
                    builder.append(replacement);
                } else {
                    builder.append(str, pos_start, pos_end + 1);
                }
                pos_start = str.indexOf("${", pos_end + 1);
            }
            builder.append(str, pos_end + 1, str.length());
            result = builder.toString();
        }
        return result;
    }

    /**
     * Initialize daemon.
     *
     * @throws Exception Fatal initialization error
     */
    public void init() throws Exception {
        /**
         * main方法第一步：初始化类加载器
         * 默认本地开发，就加载了是三个相同的类加载器 commonLoader catalinaLoader  sharedLoader
         * 可以通过catalina配置，可以初始化出不同的类加载器，相应的加载不同的url
         * CommonLoader：加载Tomcat所需要的jar包和class文件，可以被Tomcat容器本身以及各个Webapp访问；
         * CatalinaLoader：Tomcat容器私有的类加载器，加载路径中的class对于Webapp不可见；
         * SharedLoader：各个Webapp共享的类加载器，加载路径中的class对于所有Webapp可见，对于Tomcat不可见；
         *  使用了 ClassLoaderFactory 一个类完成了加载3个加载器的创建逻辑
         *
         */
        initClassLoaders();
        //sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println("设置前的上下文类加载器："+Thread.currentThread().getContextClassLoader());
        /**
         * liang17 start-preinit 上下文类加载器
         * catalinaLoader就是server加载器,设置为当前线程的上下文类加载器
         * 前面加载的就是 AppClassLoader
         * 后面加载的类的加载器还是  AppClassLoader
         * 想要使用上下文类加载器，就要显示加载
         * 另外SPI机制中，就是使用上下文类加载器加载实现类
         */
        Thread.currentThread().setContextClassLoader(catalinaLoader);
        //todo
        //类加载器机制待详细研究

        //java.net.URLClassLoader@2cb4c3ab
        System.out.println("设置前的上下文类加载器："+Thread.currentThread().getContextClassLoader());
        /**
         * 默认是安全的，设置了类加载的路径，不需要加载具体的类路径，可以去掉
         */
//        Class<?> testClass =Thread.currentThread().getContextClassLoader().loadClass("" +
//                ".lgh.comsumer.Test");
//         Class.forName("org.lgh.comsumer.Test",false,catalinaLoader);
//        System.out.println(testClass.getClassLoader());
        SecurityClassLoad.securityClassLoad(catalinaLoader);

        // Load our startup class and call its process() method
        // 为了扩展，写一堆反射，实际上就是new Catalina 并setParentClassLoader
        if (log.isTraceEnabled()) {
            log.trace("Loading startup class");
        }
        Class<?> startupClass = catalinaLoader.loadClass("org.lgh.catalina.startup.Catalina");
        System.out.println(startupClass.getClassLoader());
        Object startupInstance = startupClass.getConstructor().newInstance();

        // Set the shared extensions class loader
        if (log.isTraceEnabled()) {
            log.trace("Setting startup class properties");
        }
        String methodName = "setParentClassLoader";
        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Class.forName("java.lang.ClassLoader");
        Object paramValues[] = new Object[1];
        paramValues[0] = sharedLoader;
        Method method = startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);

        catalinaDaemon = startupInstance;
    }

    /**
     * Load daemon.
     */
    private void load(String[] arguments) throws Exception {

        // Call the load() method
        String methodName = "load";
        Object param[];
        Class<?> paramTypes[];
        if (arguments == null || arguments.length == 0) {
            paramTypes = null;
            param = null;
        } else {
            paramTypes = new Class[1];
            paramTypes[0] = arguments.getClass();
            param = new Object[1];
            param[0] = arguments;
        }
        Method method = catalinaDaemon.getClass().getMethod(methodName, paramTypes);
        if (log.isTraceEnabled()) {
            log.trace("Calling startup class " + method);
        }
        method.invoke(catalinaDaemon, param);
    }

    /**
     * getServer() for configtest
     */
    private Object getServer() throws Exception {

        String methodName = "getServer";
        Method method = catalinaDaemon.getClass().getMethod(methodName);
        return method.invoke(catalinaDaemon);
    }


    // ----------------------------------------------------------- Main Program


    /**
     * Load the Catalina daemon.
     *
     * @param arguments Initialization arguments
     *
     * @throws Exception Fatal initialization error
     */
    public void init(String[] arguments) throws Exception {

        init();
        load(arguments);
    }


    /**
     * Start the Catalina daemon.
     *
     * @throws Exception Fatal start error
     */
    public void start() throws Exception {
        if (catalinaDaemon == null) {
            init();
        }

        Method method = catalinaDaemon.getClass().getMethod("start", (Class[]) null);
        method.invoke(catalinaDaemon, (Object[]) null);
    }


    /**
     * Stop the Catalina Daemon.
     *
     * @throws Exception Fatal stop error
     */
    public void stop() throws Exception {
        Method method = catalinaDaemon.getClass().getMethod("stop", (Class[]) null);
        method.invoke(catalinaDaemon, (Object[]) null);
    }


    /**
     * Stop the standalone server.
     *
     * @throws Exception Fatal stop error
     */
    public void stopServer() throws Exception {

        Method method = catalinaDaemon.getClass().getMethod("stopServer", (Class[]) null);
        method.invoke(catalinaDaemon, (Object[]) null);
    }


    /**
     * Stop the standalone server.
     *
     * @param arguments Command line arguments
     *
     * @throws Exception Fatal stop error
     */
    public void stopServer(String[] arguments) throws Exception {

        Object param[];
        Class<?> paramTypes[];
        if (arguments == null || arguments.length == 0) {
            paramTypes = null;
            param = null;
        } else {
            paramTypes = new Class[1];
            paramTypes[0] = arguments.getClass();
            param = new Object[1];
            param[0] = arguments;
        }
        Method method = catalinaDaemon.getClass().getMethod("stopServer", paramTypes);
        method.invoke(catalinaDaemon, param);
    }


    /**
     * Set flag.
     *
     * @param await <code>true</code> if the daemon should block
     *
     * @throws Exception Reflection error
     */
    public void setAwait(boolean await) throws Exception {

        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Boolean.TYPE;
        Object paramValues[] = new Object[1];
        paramValues[0] = Boolean.valueOf(await);
        Method method = catalinaDaemon.getClass().getMethod("setAwait", paramTypes);
        method.invoke(catalinaDaemon, paramValues);
    }

    public boolean getAwait() throws Exception {
        Class<?> paramTypes[] = new Class[0];
        Object paramValues[] = new Object[0];
        Method method = catalinaDaemon.getClass().getMethod("getAwait", paramTypes);
        Boolean b = (Boolean) method.invoke(catalinaDaemon, paramValues);
        return b.booleanValue();
    }


    /**
     * Destroy the Catalina Daemon.
     */
    public void destroy() {
    }


    public static void main(String[] args) {
        /**
         *  liang12 start-preinit jdk日志的使用
         * 只要logging包下的3个类和一个接口即可使用JDK的日志框架
         * 可以做一些扩展，tomcat不会这么用，因为需要通过系统参数来设置实现类、配置文件及格式类
         * tomcat会在init方法中初始化新的日志框架实例，通过读日志配置文件来设置各项参数
         * 但是前期还是使用jdk的日志框架实例
         *  最终调用的时候就是获取level信息、handler信息及包信息，格式信息，进行最终日志记录
         */
        log.info("开始启动！");

        synchronized (daemonLock) {
            if (daemon == null) {
                // Don't set daemon until init() has completed
                Bootstrap bootstrap = new Bootstrap();
                try {
                    /**
                     * liang13 start-preinit 初始化类加载器
                     * main方法第一步：初始化类加载器
                     * 默认本地开发，就加载了是三个相同的类加载器 commonLoader catalinaLoader  sharedLoader
                     * 可以通过catalina配置，可以初始化出不同的类加载器，相应的加载不同的url
                     * CommonLoader：加载Tomcat所需要的jar包和class文件，可以被Tomcat容器本身以及各个Webapp访问；
                     *      从配置看，只能加载根目录下lib包下的类
                     * CatalinaLoader：Tomcat容器私有的类加载器，加载路径中的class对于Webapp不可见；
                     * SharedLoader：各个Webapp共享的类加载器，加载路径中的class对于所有Webapp可见，对于Tomcat不可见；
                     *  使用了 ClassLoaderFactory 一个类完成了加载3个加载器的创建逻辑
                     *
                     *  类加载器，有多种方式控制（直接继承classLoader，重写findClass方法，自定义加载可
                     *  加载的类路径，同时通过super(null)打破双亲委派机制）；二是通过new URLClassLoader,也是通过构造器传入
                     *  空的父加载器即可打破双亲委派机制，关键在于加载路径，在catalina配置文件中配置
                     *  common.loader="${catalina.base}/target/classes",
                     *
                     */
                    bootstrap.init();
                } catch (Throwable t) {
                    handleThrowable(t);
                    log.error("Init exception", t);
                    return;
                }
                daemon = bootstrap;
            } else {
                // When running as a service the call to stop will be on a new
                // thread so make sure the correct class loader is used to
                // prevent a range of class not found exceptions.
                Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
            }
        }
        /**
         *  以上初始化了bootstrap及Catalina，并设置了他的父类加载器
         *  ========================================================================
         */

        /**
         * liang18 start-preinit 响应命令
         * 在edit configuration中的program arguments中输入参数
         * 这里可以响应 start startd stopd stop configtest
         * 默认start
         */
        try {
            //获取启动参数，默认start
            String command = "start";
            if (args.length > 0) {
                command = args[args.length - 1];
            }

            if (command.equals("startd")) {
                args[args.length - 1] = "start";
                daemon.load(args);
                daemon.start();
            } else if (command.equals("stopd")) {
                args[args.length - 1] = "stop";
                daemon.stop();
            } else if (command.equals("start")) {
                daemon.setAwait(true);
                /**
                 * 第二步 bootstrap的加载 ，并调用了catalina的load()方法
                 */
                daemon.load(args);//程序执行到liang41，调了一系列的init或者internal方法
                daemon.start();
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
            } else if (command.equals("stop")) {
                daemon.stopServer(args);
            } else if (command.equals("configtest")) {
                daemon.load(args);
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
                System.exit(0);
            } else {
                log.warn("Bootstrap: command \"" + command + "\" does not exist.");
            }
        } catch (Throwable t) {
            // Unwrap the Exception for clearer error reporting
            if (t instanceof InvocationTargetException && t.getCause() != null) {
                t = t.getCause();
            }
            handleThrowable(t);
            log.error("Error running command", t);
            System.exit(1);
        }

    }

    /**
     * Obtain the name of configured home (binary) directory. Note that home and base may be the same (and are by
     * default).
     *
     * @return the catalina home
     */
    public static String getCatalinaHome() {
        return catalinaHomeFile.getPath();
    }


    /**
     * Obtain the name of the configured base (instance) directory. Note that home and base may be the same (and are by
     * default). If this is not set the value returned by {@link #getCatalinaHome()} will be used.
     *
     * @return the catalina base
     */
    public static String getCatalinaBase() {
        return catalinaBaseFile.getPath();
    }


    /**
     * Obtain the configured home (binary) directory. Note that home and base may be the same (and are by default).
     *
     * @return the catalina home as a file
     */
    public static File getCatalinaHomeFile() {
        return catalinaHomeFile;
    }


    /**
     * Obtain the configured base (instance) directory. Note that home and base may be the same (and are by default). If
     * this is not set the value returned by {@link #getCatalinaHomeFile()} will be used.
     *
     * @return the catalina base as a file
     */
    public static File getCatalinaBaseFile() {
        return catalinaBaseFile;
    }


    // Copied from ExceptionUtils since that class is not visible during start
    static void handleThrowable(Throwable t) {
        if (t instanceof ThreadDeath) {
            throw (ThreadDeath) t;
        }
        if (t instanceof StackOverflowError) {
            // Swallow silently - it should be recoverable
            return;
        }
        if (t instanceof VirtualMachineError) {
            throw (VirtualMachineError) t;
        }
        // All other instances of Throwable will be silently swallowed
    }

    // Copied from ExceptionUtils so that there is no dependency on utils
    static Throwable unwrapInvocationTargetException(Throwable t) {
        if (t instanceof InvocationTargetException && t.getCause() != null) {
            return t.getCause();
        }
        return t;
    }

    // Protected for unit testing
    protected static String[] getPaths(String value) {

        List<String> result = new ArrayList<>();
        Matcher matcher = PATH_PATTERN.matcher(value);

        while (matcher.find()) {
            String path = value.substring(matcher.start(), matcher.end());

            path = path.trim();
            if (path.length() == 0) {
                continue;
            }

            char first = path.charAt(0);
            char last = path.charAt(path.length() - 1);

            if (first == '"' && last == '"' && path.length() > 1) {
                path = path.substring(1, path.length() - 1);
                path = path.trim();
                if (path.length() == 0) {
                    continue;
                }
            } else if (path.contains("\"")) {
                // Unbalanced quotes
                // Too early to use standard i18n support. The class path hasn't
                // been configured.
                throw new IllegalArgumentException(
                        "The double quote [\"] character can only be used to quote paths. It must " +
                                "not appear in a path. This loader path is not valid: [" + value + "]");
            } else {
                // Not quoted - NO-OP
            }

            result.add(path);
        }

        return result.toArray(new String[0]);
    }
}
